index.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. // @ts-nocheck
  2. import { PermissionAction } from '@supabase/shared-types/out/constants'
  3. import { useParams } from 'common'
  4. import { partition } from 'lodash'
  5. import { MessageCircle } from 'lucide-react'
  6. import { useRouter } from 'next/router'
  7. import { useState } from 'react'
  8. import { toast } from 'sonner'
  9. import { Button } from 'ui'
  10. import { Overview } from '@/components/interfaces/BranchManagement/Overview'
  11. import BranchLayout from '@/components/layouts/BranchLayout/BranchLayout'
  12. import { DefaultLayout } from '@/components/layouts/DefaultLayout'
  13. import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
  14. import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
  15. import { AlertError } from '@/components/ui/AlertError'
  16. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  17. import { DocsButton } from '@/components/ui/DocsButton'
  18. import { NoPermission } from '@/components/ui/NoPermission'
  19. import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
  20. import { useBranchDeleteMutation } from '@/data/branches/branch-delete-mutation'
  21. import { Branch, useBranchesQuery } from '@/data/branches/branches-query'
  22. import { useGitHubConnectionsQuery } from '@/data/integrations/github-connections-query'
  23. import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
  24. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  25. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  26. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  27. import { DOCS_URL } from '@/lib/constants'
  28. import { useAppStateSnapshot } from '@/state/app-state'
  29. import type { NextPageWithLayout } from '@/types'
  30. const BranchesPage: NextPageWithLayout = () => {
  31. const router = useRouter()
  32. const { ref } = useParams()
  33. const snap = useAppStateSnapshot()
  34. const { data: project } = useSelectedProjectQuery()
  35. const { data: selectedOrg } = useSelectedOrganizationQuery()
  36. const [selectedBranchToDelete, setSelectedBranchToDelete] = useState<Branch>()
  37. const { mutate: sendEvent } = useSendEventMutation()
  38. const isBranch = project?.parent_project_ref !== undefined
  39. const projectRef =
  40. project !== undefined ? (isBranch ? project.parent_project_ref : ref) : undefined
  41. const { can: canReadBranches, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
  42. PermissionAction.READ,
  43. 'preview_branches'
  44. )
  45. const {
  46. data: connections,
  47. error: connectionsError,
  48. isPending: isLoadingConnections,
  49. isSuccess: isSuccessConnections,
  50. isError: isErrorConnections,
  51. } = useGitHubConnectionsQuery({
  52. organizationId: selectedOrg?.id,
  53. })
  54. const {
  55. data: branches,
  56. error: branchesError,
  57. isPending: isLoadingBranches,
  58. isError: isErrorBranches,
  59. isSuccess: isSuccessBranches,
  60. } = useBranchesQuery({ projectRef })
  61. const [[mainBranch], previewBranchesUnsorted] = partition(branches, (branch) => branch.is_default)
  62. const previewBranches = previewBranchesUnsorted.sort((a, b) =>
  63. new Date(a.updated_at) < new Date(b.updated_at) ? 1 : -1
  64. )
  65. const githubConnection = connections?.find((connection) => connection.project.ref === projectRef)
  66. const repo = githubConnection?.repository.name ?? ''
  67. const isError = isErrorConnections || isErrorBranches
  68. const isLoading = isLoadingConnections || isLoadingBranches
  69. const isSuccess = isSuccessConnections && isSuccessBranches
  70. const isGithubConnected = githubConnection !== undefined
  71. const { mutate: deleteBranch, isPending: isDeleting } = useBranchDeleteMutation({
  72. onSuccess: () => {
  73. toast.success('Successfully deleted branch')
  74. setSelectedBranchToDelete(undefined)
  75. },
  76. })
  77. const generateCreatePullRequestURL = (branch?: string) => {
  78. if (githubConnection === undefined) return 'https://github.com'
  79. return branch !== undefined
  80. ? `https://github.com/${githubConnection.repository.name}/compare/${mainBranch?.git_branch}...${branch}`
  81. : `https://github.com/${githubConnection.repository.name}/compare`
  82. }
  83. const onConfirmDeleteBranch = () => {
  84. if (selectedBranchToDelete === undefined) return console.error('No branch selected')
  85. const {
  86. project_ref: branchRef,
  87. parent_project_ref: projectRef,
  88. persistent,
  89. } = selectedBranchToDelete
  90. deleteBranch(
  91. { branchRef, projectRef },
  92. {
  93. onSuccess: () => {
  94. if (branchRef === ref) {
  95. router.push(`/project/${projectRef}/branches`)
  96. }
  97. // Track delete button click
  98. sendEvent({
  99. action: 'branch_delete_button_clicked',
  100. properties: {
  101. branchType: persistent ? 'persistent' : 'preview',
  102. origin: 'branches_page',
  103. },
  104. groups: {
  105. project: projectRef ?? 'Unknown',
  106. organization: selectedOrg?.slug ?? 'Unknown',
  107. },
  108. })
  109. },
  110. }
  111. )
  112. }
  113. return (
  114. <>
  115. <ScaffoldContainer>
  116. <ScaffoldSection>
  117. <div className="col-span-12">
  118. <div className="space-y-4">
  119. {isPermissionsLoaded && !canReadBranches ? (
  120. <NoPermission resourceText="view this project's branches" />
  121. ) : (
  122. <>
  123. {isErrorConnections && (
  124. <AlertError
  125. error={connectionsError}
  126. subject="Failed to retrieve GitHub integration connection"
  127. />
  128. )}
  129. {isErrorBranches && (
  130. <AlertError
  131. error={branchesError}
  132. subject="Failed to retrieve preview branches"
  133. />
  134. )}
  135. {!isError && (
  136. <Overview
  137. isGithubConnected={isGithubConnected}
  138. isLoading={isLoading}
  139. isSuccess={isSuccess}
  140. repo={repo}
  141. mainBranch={mainBranch}
  142. previewBranches={previewBranches}
  143. onSelectCreateBranch={() => snap.setShowCreateBranchModal(true)}
  144. onSelectDeleteBranch={setSelectedBranchToDelete}
  145. generateCreatePullRequestURL={generateCreatePullRequestURL}
  146. />
  147. )}
  148. </>
  149. )}
  150. </div>
  151. </div>
  152. </ScaffoldSection>
  153. </ScaffoldContainer>
  154. <TextConfirmModal
  155. variant="warning"
  156. visible={selectedBranchToDelete !== undefined}
  157. onCancel={() => setSelectedBranchToDelete(undefined)}
  158. onConfirm={() => onConfirmDeleteBranch()}
  159. loading={isDeleting}
  160. title="Delete branch"
  161. confirmLabel="Delete branch"
  162. confirmPlaceholder="Type in name of branch"
  163. confirmString={selectedBranchToDelete?.name ?? ''}
  164. alert={{
  165. title: 'You cannot recover this branch once deleted',
  166. }}
  167. text={
  168. <>
  169. This will delete your database preview branch{' '}
  170. <span className="text-bold text-foreground">{selectedBranchToDelete?.name}</span>.
  171. </>
  172. }
  173. />
  174. </>
  175. )
  176. }
  177. BranchesPage.getLayout = (page) => {
  178. const BranchesPageWrapper = () => {
  179. const snap = useAppStateSnapshot()
  180. const { can: canCreateBranches } = useAsyncCheckPermissions(
  181. PermissionAction.CREATE,
  182. 'preview_branches',
  183. {
  184. resource: { is_default: false },
  185. }
  186. )
  187. const primaryActions = (
  188. <ButtonTooltip
  189. type="primary"
  190. disabled={!canCreateBranches}
  191. onClick={() => snap.setShowCreateBranchModal(true)}
  192. tooltip={{
  193. content: {
  194. side: 'bottom',
  195. text: !canCreateBranches
  196. ? 'You need additional permissions to create branches'
  197. : undefined,
  198. },
  199. }}
  200. >
  201. Create branch
  202. </ButtonTooltip>
  203. )
  204. const secondaryActions = (
  205. <div className="flex items-center gap-x-2">
  206. <Button asChild type="text" icon={<MessageCircle className="text-muted" strokeWidth={1} />}>
  207. <a
  208. target="_blank"
  209. rel="noreferrer"
  210. href="https://github.com/orgs/briven/discussions/18937"
  211. >
  212. Branching feedback
  213. </a>
  214. </Button>
  215. <DocsButton href={`${DOCS_URL}/guides/platform/branching`} />
  216. </div>
  217. )
  218. return (
  219. <PageLayout
  220. title="Branches"
  221. subtitle="Manage your database preview branches and deployments"
  222. primaryActions={primaryActions}
  223. secondaryActions={secondaryActions}
  224. >
  225. {page}
  226. </PageLayout>
  227. )
  228. }
  229. return (
  230. <DefaultLayout>
  231. <BranchLayout>
  232. <BranchesPageWrapper />
  233. </BranchLayout>
  234. </DefaultLayout>
  235. )
  236. }
  237. export default BranchesPage